/*-------------------------------------------------------------------------
 *
 * spgutils.c
 *	  various support functions for SP-GiST
 *
 *
 * Portions Copyright (c) 1996-2022, PostgreSQL Global Development Group
 * Portions Copyright (c) 1994, Regents of the University of California
 *
 * IDENTIFICATION
 *			src/backend/access/spgist/spgutils.c
 *
 *-------------------------------------------------------------------------
 */

#include "postgres.h"

#include "access/amvalidate.h"
#include "access/htup_details.h"
#include "access/reloptions.h"
#include "access/spgist_private.h"
#include "access/toast_compression.h"
#include "access/transam.h"
#include "access/xact.h"
#include "catalog/pg_amop.h"
#include "commands/vacuum.h"
#include "nodes/nodeFuncs.h"
#include "parser/parse_coerce.h"
#include "storage/bufmgr.h"
#include "storage/indexfsm.h"
#include "storage/lmgr.h"
#include "utils/builtins.h"
#include "utils/catcache.h"
#include "utils/index_selfuncs.h"
#include "utils/lsyscache.h"
#include "utils/syscache.h"


/*
 * SP-GiST 处理函数：返回带有访问方法参数和回调的 IndexAmRoutine。
 */
Datum spghandler(PG_FUNCTION_ARGS)
{
	IndexAmRoutine *fc_amroutine = makeNode(IndexAmRoutine);

	fc_amroutine->amstrategies = 0;
	fc_amroutine->amsupport = SPGISTNProc;
	fc_amroutine->amoptsprocnum = SPGIST_OPTIONS_PROC;
	fc_amroutine->amcanorder = false;
	fc_amroutine->amcanorderbyop = true;
	fc_amroutine->amcanbackward = false;
	fc_amroutine->amcanunique = false;
	fc_amroutine->amcanmulticol = false;
	fc_amroutine->amoptionalkey = true;
	fc_amroutine->amsearcharray = false;
	fc_amroutine->amsearchnulls = true;
	fc_amroutine->amstorage = true;
	fc_amroutine->amclusterable = false;
	fc_amroutine->ampredlocks = false;
	fc_amroutine->amcanparallel = false;
	fc_amroutine->amcaninclude = true;
	fc_amroutine->amusemaintenanceworkmem = false;
	fc_amroutine->amparallelvacuumoptions =
		VACUUM_OPTION_PARALLEL_BULKDEL | VACUUM_OPTION_PARALLEL_COND_CLEANUP;
	fc_amroutine->amkeytype = InvalidOid;

	fc_amroutine->ambuild = spgbuild;
	fc_amroutine->ambuildempty = spgbuildempty;
	fc_amroutine->aminsert = spginsert;
	fc_amroutine->ambulkdelete = spgbulkdelete;
	fc_amroutine->amvacuumcleanup = spgvacuumcleanup;
	fc_amroutine->amcanreturn = spgcanreturn;
	fc_amroutine->amcostestimate = spgcostestimate;
	fc_amroutine->amoptions = spgoptions;
	fc_amroutine->amproperty = spgproperty;
	fc_amroutine->ambuildphasename = NULL;
	fc_amroutine->amvalidate = spgvalidate;
	fc_amroutine->amadjustmembers = spgadjustmembers;
	fc_amroutine->ambeginscan = spgbeginscan;
	fc_amroutine->amrescan = spgrescan;
	fc_amroutine->amgettuple = spggettuple;
	fc_amroutine->amgetbitmap = spggetbitmap;
	fc_amroutine->amendscan = spgendscan;
	fc_amroutine->ammarkpos = NULL;
	fc_amroutine->amrestrpos = NULL;
	fc_amroutine->amestimateparallelscan = NULL;
	fc_amroutine->aminitparallelscan = NULL;
	fc_amroutine->amparallelrescan = NULL;

	PG_RETURN_POINTER(fc_amroutine);
}

/*
 * GetIndexInputType
 *		确定索引列的名义输入数据类型
 *
 * 我们将“名义”输入类型定义为相关 opclass 的 opcintype，或者如果这是一个多态类型，则为堆列或表达式的基本类型，该类型是索引的输入。选择 opcintype 的原因是非多态 opclass 可能不希望了解二进制兼容的输入类型。例如，如果一个文本 opclass 与 varchar 堆列一起使用，我们希望报告“text”而不是“varchar”。同样，opclass 不希望了解域类型，因此如果我们确实咨询实际输入类型，我们会确保扁平化域。
 *
 * 某些时候，也许这个应该放在其他地方，但不清楚其他索引 AM 是否会对此有用。
 */
static Oid fc_GetIndexInputType(Relation fc_index, AttrNumber fc_indexcol)
{
	Oid			fc_opcintype;
	AttrNumber	fc_heapcol;
	List	   *fc_indexprs;
	ListCell   *fc_indexpr_item;

	Assert(fc_index->rd_index != NULL);
	Assert(fc_indexcol > 0 && fc_indexcol <= fc_index->rd_index->indnkeyatts);
	fc_opcintype = fc_index->rd_opcintype[fc_indexcol - 1];
	if (!IsPolymorphicType(fc_opcintype))
		return fc_opcintype;
	fc_heapcol = fc_index->rd_index->indkey.values[fc_indexcol - 1];
	if (fc_heapcol != 0)			/* 简单索引列？ */
		return getBaseType(get_atttype(fc_index->rd_index->indrelid, fc_heapcol));

	/*
	 * 如果索引表达式已经缓存，则跳过调用 RelationGetIndexExpressions，因为它会做一个多余的副本。
	 * 我们不会修改树，也不会做任何在完成之前会使 relcache 条目失效的事情。
	 */
	if (fc_index->rd_indexprs)
		fc_indexprs = fc_index->rd_indexprs;
	else
		fc_indexprs = RelationGetIndexExpressions(fc_index);
	fc_indexpr_item = list_head(fc_indexprs);
	for (int fc_i = 1; fc_i <= fc_index->rd_index->indnkeyatts; fc_i++)
	{
		if (fc_index->rd_index->indkey.values[fc_i - 1] == 0)
		{
			/* 表达式列 */
			if (fc_indexpr_item == NULL)
				elog(ERROR, "wrong number of index expressions");
			if (fc_i == fc_indexcol)
				return getBaseType(exprType((Node *) lfirst(fc_indexpr_item)));
			fc_indexpr_item = lnext(fc_indexprs, fc_indexpr_item);
		}
	}
	elog(ERROR, "wrong number of index expressions");
	return InvalidOid;			/* 保持编译器安静 */
}

/* 填充 SpGistTypeDesc 结构体，包含有关指定数据类型的信息 */
static void fc_fillTypeDesc(SpGistTypeDesc *fc_desc, Oid fc_type)
{
	HeapTuple	fc_tp;
	Form_pg_type fc_typtup;

	fc_desc->type = fc_type;
	fc_tp = SearchSysCache1(TYPEOID, ObjectIdGetDatum(fc_type));
	if (!HeapTupleIsValid(fc_tp))
		elog(ERROR, "cache lookup failed for type %u", fc_type);
	fc_typtup = (Form_pg_type) GETSTRUCT(fc_tp);
	fc_desc->attlen = fc_typtup->typlen;
	fc_desc->attbyval = fc_typtup->typbyval;
	fc_desc->attalign = fc_typtup->typalign;
	fc_desc->attstorage = fc_typtup->typstorage;
	ReleaseSysCache(fc_tp);
}

/*
 * 获取索引的 AM 特定信息的本地缓存，如果必要，初始化它
 */
SpGistCache *
spgGetCache(Relation fc_index)
{
	SpGistCache *fc_cache;

	if (fc_index->rd_amcache == NULL)
	{
		Oid			fc_atttype;
		spgConfigIn fc_in;
		FmgrInfo   *fc_procinfo;

		fc_cache = MemoryContextAllocZero(fc_index->rd_indexcxt,
									   sizeof(SpGistCache));

		/* SPGiST 必须有一个键列，也可以有 INCLUDE 列 */
		Assert(IndexRelationGetNumberOfKeyAttributes(fc_index) == 1);
		Assert(IndexRelationGetNumberOfAttributes(fc_index) <= INDEX_MAX_KEYS);

		/*
		 * 获取键列的实际（好吧，名义）数据类型。
		 * 我们将其传递给 opclass 配置函数，以使多态 opclass 成为可能。
		 */
		fc_atttype = fc_GetIndexInputType(fc_index, spgKeyColumn + 1);

		/* 调用配置函数以获取 opclass 的配置信息 */
		fc_in.attType = fc_atttype;

		fc_procinfo = index_getprocinfo(fc_index, 1, SPGIST_CONFIG_PROC);
		FunctionCall2Coll(fc_procinfo,
						  fc_index->rd_indcollation[spgKeyColumn],
						  PointerGetDatum(&fc_in),
						  PointerGetDatum(&fc_cache->config));

		/*
		 * 如果未指定 leafType，请使用声明的索引列类型，
		 * index.c 会从 opclass 的 opcintype 派生该类型。
		 * （尽管我们现在使 spgvalidate.c 在这些不相同时发出警告，
		 * 但旧的用户定义 opclass 可能未正确设置 STORAGE 参数，
		 * 所以如果给定了 leafType，则相信它。）
		 */
		if (!OidIsValid(fc_cache->config.leafType))
		{
			fc_cache->config.leafType =
				TupleDescAttr(RelationGetDescr(fc_index), spgKeyColumn)->atttypid;

			/*
			 * 如果索引列类型可以强制转换为 atttype（例如，这是 atttype 的域），
			 * 将其视为普通的 atttype，以避免认为我们需要压缩。
			 */
			if (fc_cache->config.leafType != fc_atttype &&
				IsBinaryCoercible(fc_cache->config.leafType, fc_atttype))
				fc_cache->config.leafType = fc_atttype;
		}

		/* 获取我们需要的每个相关数据类型的信息 */
		fc_fillTypeDesc(&fc_cache->attType, fc_atttype);

		if (fc_cache->config.leafType != fc_atttype)
		{
			if (!OidIsValid(index_getprocid(fc_index, 1, SPGIST_COMPRESS_PROC)))
				ereport(ERROR,
						(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
						 errmsg("compress method must be defined when leaf type is different from input type")));

			fc_fillTypeDesc(&fc_cache->attLeafType, fc_cache->config.leafType);
		}
		else
		{
			/* 在这种常见情况下保存查找 */
			fc_cache->attLeafType = fc_cache->attType;
		}

		fc_fillTypeDesc(&fc_cache->attPrefixType, fc_cache->config.prefixType);
		fc_fillTypeDesc(&fc_cache->attLabelType, fc_cache->config.labelType);

		/*
		 * 最后，如果这是一个真实的索引（而不是分区索引），请从元页面获取 lastUsedPages 数据
		 */
		if (fc_index->rd_rel->relkind != RELKIND_PARTITIONED_INDEX)
		{
			Buffer		fc_metabuffer;
			SpGistMetaPageData *fc_metadata;

			fc_metabuffer = ReadBuffer(fc_index, SPGIST_METAPAGE_BLKNO);
			LockBuffer(fc_metabuffer, BUFFER_LOCK_SHARE);

			fc_metadata = SpGistPageGetMeta(BufferGetPage(fc_metabuffer));

			if (fc_metadata->magicNumber != SPGIST_MAGIC_NUMBER)
				elog(ERROR, "index \"%s\" is not an SP-GiST index",
					 RelationGetRelationName(fc_index));

			fc_cache->lastUsedPages = fc_metadata->lastUsedPages;

			UnlockReleaseBuffer(fc_metabuffer);
		}

		fc_index->rd_amcache = (void *) fc_cache;
	}
	else
	{
		/* 假设它是最新的 */
		fc_cache = (SpGistCache *) fc_index->rd_amcache;
	}

	return fc_cache;
}

/*
 * 计算叶元组或仅索引扫描结果元组的元组描述符。
 *
 * 在许多情况下，我们可以直接使用 relcache 的 tupdesc，并且
 * 就 INCLUDE 列而言，这通常是可以的。然而，关键列的条目必须在第一种情况下与 leafType 匹配，
 * 或在第二种情况下与 attType 匹配。虽然 relcache 的 tupdesc *应该* 显示 leafType，
 * 但对于遗留的用户定义 opclasses，这可能不成立，因为在 v14 之前，它们不允许在 CREATE OPCLASS 中声明其真实的存储类型。
 * 此外，attType 可能与 relcache 中的内容不同。
 *
 * 该函数返回一个指向 relcache 的 tupdesc 的指针（如果适合），
 * 或一个经过调整以匹配指定关键列类型的 palloc'd 副本。通过要求调用者传递 SpGistTypeDesc 而不仅仅是 OID，
 * 我们可以避免在这里进行任何目录查找。
 */
TupleDesc getSpGistTupleDesc(Relation fc_index, SpGistTypeDesc *fc_keyType)
{
	TupleDesc	fc_outTupDesc;
	Form_pg_attribute fc_att;

	if (fc_keyType->type ==
		TupleDescAttr(RelationGetDescr(fc_index), spgKeyColumn)->atttypid)
		fc_outTupDesc = RelationGetDescr(fc_index);
	else
	{
		fc_outTupDesc = CreateTupleDescCopy(RelationGetDescr(fc_index));
		fc_att = TupleDescAttr(fc_outTupDesc, spgKeyColumn);
		/* 更新列的类型相关字段即可 */
		fc_att->atttypid = fc_keyType->type;
		fc_att->atttypmod = -1;
		fc_att->attlen = fc_keyType->attlen;
		fc_att->attbyval = fc_keyType->attbyval;
		fc_att->attalign = fc_keyType->attalign;
		fc_att->attstorage = fc_keyType->attstorage;
		/* 我们不需要费心使这些有效： */
		fc_att->attcompression = InvalidCompressionMethod;
		fc_att->attcollation = InvalidOid;
		/* 如果我们改变了 typlen，最好重置后面的偏移量 */
		for (int fc_i = spgFirstIncludeColumn; fc_i < fc_outTupDesc->natts; fc_i++)
			TupleDescAttr(fc_outTupDesc, fc_i)->attcacheoff = -1;
	}
	return fc_outTupDesc;
}

/* 初始化 SpGistState 以处理给定的索引 */
void initSpGistState(SpGistState *fc_state, Relation fc_index)
{
	SpGistCache *fc_cache;

	fc_state->index = fc_index;

	/* 获取有关索引的缓存静态信息 */
	fc_cache = spgGetCache(fc_index);

	fc_state->config = fc_cache->config;
	fc_state->attType = fc_cache->attType;
	fc_state->attLeafType = fc_cache->attLeafType;
	fc_state->attPrefixType = fc_cache->attPrefixType;
	fc_state->attLabelType = fc_cache->attLabelType;

	/* 确保我们有一个有效的叶元组描述符 */
	fc_state->leafTupDesc = getSpGistTupleDesc(fc_state->index, &fc_state->attLeafType);

	/* 为构造死元组创建工作空间 */
	fc_state->deadTupleStorage = palloc0(SGDTSIZE);

	/*
	 * 设置用于重定向元组的地平线 XID。如果我们有自己的 XID，则使用它，
	 * 否则使用 InvalidTransactionId。后一种情况可能发生在 VACUUM 或 REINDEX CONCURRENTLY 中，
	 * 在这两种情况下都不可以强制分配 XID。无论如何，VACUUM 不会创建任何重定向元组，
	 * 但 REINDEX CONCURRENTLY 可以。幸运的是，REINDEX CONCURRENTLY 在结束之前不会将索引标记为有效，
	 * 因此在它插入重定向元组时，不可能有任何并发扫描“处于飞行中”。
	 * 并且它会在结束之前锁定 VACUUM。因此，VACUUM 可以立即失效包含无效 xid 的重定向元组。
	 */
	fc_state->myXid = GetTopTransactionIdIfAny();

	/* 假设我们不在索引构建中（spgbuild 将覆盖） */
	fc_state->isBuild = false;
}

/*
 * 分配一个新页面（通过回收或扩展索引文件）。
 *
 * 返回的缓冲区已经固定并独占锁定。
 * 调用者负责通过调用 SpGistInitBuffer 初始化页面。
 */
Buffer SpGistNewBuffer(Relation fc_index)
{
	Buffer		fc_buffer;
	bool		fc_needLock;

	/* 首先，尝试从 FSM 获取一个页面 */
	for (;;)
	{
		BlockNumber fc_blkno = GetFreeIndexPage(fc_index);

		if (fc_blkno == InvalidBlockNumber)
			break;				/* FSM 不了解的内容 */

		/*
		 * 固定页面不应该被列在 FSM 中，但以防万一，
		 * 如果有一个，将其忽略。
		 */
		if (SpGistBlockIsFixed(fc_blkno))
			continue;

		fc_buffer = ReadBuffer(fc_index, fc_blkno);

		/*
		 * 我们必须防止其他人已经回收此页面的可能性；
		 * 如果这样，缓冲区可能会被锁定。
		 */
		if (ConditionalLockBuffer(fc_buffer))
		{
			Page		fc_page = BufferGetPage(fc_buffer);

			if (PageIsNew(fc_page))
				return fc_buffer;	/* 可以使用，如果从未初始化 */

			if (SpGistPageIsDeleted(fc_page) || PageIsEmpty(fc_page))
				return fc_buffer;	/* 可以使用 */

			LockBuffer(fc_buffer, BUFFER_LOCK_UNLOCK);
		}

		/* 不能使用，因此释放缓冲区并重试 */
		ReleaseBuffer(fc_buffer);
	}

	/* 必须扩展文件 */
	fc_needLock = !RELATION_IS_LOCAL(fc_index);
	if (fc_needLock)
		LockRelationForExtension(fc_index, ExclusiveLock);

	fc_buffer = ReadBuffer(fc_index, P_NEW);
	LockBuffer(fc_buffer, BUFFER_LOCK_EXCLUSIVE);

	if (fc_needLock)
		UnlockRelationForExtension(fc_index, ExclusiveLock);

	return fc_buffer;
}

/*
 * 从本地缓存更新索引元页面的 lastUsedPages 信息，如果可能的话
 *
 * 更新元页面对索引工作不是至关重要，因此
 * 1 使用 ConditionalLockBuffer 来提高并发性
 * 2 不将 metabuffer 更改写入 WAL，以减少 WAL 流量
 */
void SpGistUpdateMetaPage(Relation fc_index)
{
	SpGistCache *fc_cache = (SpGistCache *) fc_index->rd_amcache;

	if (fc_cache != NULL)
	{
		Buffer		fc_metabuffer;

		fc_metabuffer = ReadBuffer(fc_index, SPGIST_METAPAGE_BLKNO);

		if (ConditionalLockBuffer(fc_metabuffer))
		{
			Page		fc_metapage = BufferGetPage(fc_metabuffer);
			SpGistMetaPageData *fc_metadata = SpGistPageGetMeta(fc_metapage);

			fc_metadata->lastUsedPages = fc_cache->lastUsedPages;

			/*
			 * 设置 pd_lower 在元数据末尾之后。这是
			 * 必不可少的，因为如果不这样做，如果 xlog.c 压缩了页面，
			 * 元数据将会丢失。（我们必须在这里这样做，因为
			 * 早于 v11 版本的 PG 并未正确设置 metapage 的 pd_lower，
			 * 因此一个 pg_upgraded 索引可能包含错误的
			 * 值。）
			 */
			((PageHeader) fc_metapage)->pd_lower =
				((char *) fc_metadata + sizeof(SpGistMetaPageData)) - (char *) fc_metapage;

			MarkBufferDirty(fc_metabuffer);
			UnlockReleaseBuffer(fc_metabuffer);
		}
		else
		{
			ReleaseBuffer(fc_metabuffer);
		}
	}
}

/* 宏以根据标志选择 lastUsedPages 缓存的正确元素 */
/* 使用 SPGIST_CACHED_PAGES 对标志进行掩码仅仅是出于 paranoid 的考虑 */
#define GET_LUP(c, f)  (&(c)->lastUsedPages.cachedPage[((unsigned int) (f)) % SPGIST_CACHED_PAGES])

/*
 * 分配并初始化由标志指定的类型和奇偶性的新缓存区。
 * 返回的缓存区已经被锁定并独占锁定。
 *
 * 在请求内部页面时，如果我们得到的是错误的奇偶性，
 * 我们只需释放缓存区并重试。我们将获得一个不同的页面，
 * 因为 GetFreeIndexPage 会在 FSM 中将该页面标记为使用过。
 * 该页面已被添加到我们的本地 lastUsedPages 缓存中，
 * 因此我们希望在此次会话中稍后使用它，但否则我们依赖 VACUUM
 * 最终重新将该页面重新加入 FSM，使其可用于回收。
 * 注意，这样的页面在这里不会被标记为脏，因此除非它被
 * 及时使用，缓存区将被丢弃，该页面将在磁盘上保持原样。
 *
 * 当我们将缓存区返回给调用者时，页面 *不* 会被加入到
 * lastUsedPages 缓存中；我们希望调用者在使用了它将使用的
 * 空间后进行此操作。这是因为在调用者耗尽了一些空间后，
 * 该页面可能可用的空间少于已经缓存的空间，因此我们宁愿不损坏旧的缓存项。
 */
static Buffer fc_allocNewBuffer(Relation fc_index, int fc_flags)
{
	SpGistCache *fc_cache = spgGetCache(fc_index);
	uint16		fc_pageflags = 0;

	if (GBUF_REQ_LEAF(fc_flags))
		fc_pageflags |= SPGIST_LEAF;
	if (GBUF_REQ_NULLS(fc_flags))
		fc_pageflags |= SPGIST_NULLS;

	for (;;)
	{
		Buffer		fc_buffer;

		fc_buffer = SpGistNewBuffer(fc_index);
		SpGistInitBuffer(fc_buffer, fc_pageflags);

		if (fc_pageflags & SPGIST_LEAF)
		{
			/* 叶子页面没有奇偶性问题，所以直接使用 */
			return fc_buffer;
		}
		else
		{
			BlockNumber fc_blkno = BufferGetBlockNumber(fc_buffer);
			int			fc_blkFlags = GBUF_INNER_PARITY(fc_blkno);

			if ((fc_flags & GBUF_PARITY_MASK) == fc_blkFlags)
			{
				/* 页面具有正确的奇偶性，使用它 */
				return fc_buffer;
			}
			else
			{
				/* 页面具有错误的奇偶性，将其记录在缓存中并重试 */
				if (fc_pageflags & SPGIST_NULLS)
					fc_blkFlags |= GBUF_NULLS;
				fc_cache->lastUsedPages.cachedPage[fc_blkFlags].blkno = fc_blkno;
				fc_cache->lastUsedPages.cachedPage[fc_blkFlags].freeSpace =
					PageGetExactFreeSpace(BufferGetPage(fc_buffer));
				UnlockReleaseBuffer(fc_buffer);
			}
		}
	}
}

/*
 * 获取类型和奇偶性由标志指定的缓存区，至少
 * 与 needSpace 指示的可用空间相同。我们使用 lastUsedPages
 * 缓存来尽可能分配之前请求的相同缓存区。
 * 返回的缓存区已经被锁定并独占锁定。
 *
 * *isNew 在页面在此处初始化时设置为 true，
 * 如果它已经有效，则设置为 false。
 */
Buffer SpGistGetBuffer(Relation fc_index, int fc_flags, int fc_needSpace, bool *fc_isNew)
{
	SpGistCache *fc_cache = spgGetCache(fc_index);
	SpGistLastUsedPage *fc_lup;

	/* 如果即使是一个空页面也无法满足需求则退出 */
	if (fc_needSpace > SPGIST_PAGE_CAPACITY)
		elog(ERROR, "desired SPGiST tuple size is too big");

	/*
	 * 如果可能，增加空间请求以包含关系的
	 * fillfactor。这确保当我们向页面添加不相关的元组时，
	 * 我们尝试保持 100-fillfactor% 可用于添加与
	 * 已经在页面上的元组相关的元组。但 fillfactor 不应导致
	 * 对本来合法的请求出现错误。
	 */
	fc_needSpace += SpGistGetTargetPageFreeSpace(fc_index);
	fc_needSpace = Min(fc_needSpace, SPGIST_PAGE_CAPACITY);

	/* 获取该标志设置的缓存条目 */
	fc_lup = GET_LUP(fc_cache, fc_flags);

	/* 如果我们没有缓存的内容，就交给 allocNewBuffer 处理 */
	if (fc_lup->blkno == InvalidBlockNumber)
	{
		*fc_isNew = true;
		return fc_allocNewBuffer(fc_index, fc_flags);
	}

	/* 固定页面不应出现在缓存中 */
	Assert(!SpGistBlockIsFixed(fc_lup->blkno));

	/* 如果缓存的 freeSpace 不够，就不必查看页面 */
	if (fc_lup->freeSpace >= fc_needSpace)
	{
		Buffer		fc_buffer;
		Page		fc_page;

		fc_buffer = ReadBuffer(fc_index, fc_lup->blkno);

		if (!ConditionalLockBuffer(fc_buffer))
		{
			/*
			 * 缓冲区被另一个进程锁定，因此返回一个新的缓冲区
			 */
			ReleaseBuffer(fc_buffer);
			*fc_isNew = true;
			return fc_allocNewBuffer(fc_index, fc_flags);
		}

		fc_page = BufferGetPage(fc_buffer);

		if (PageIsNew(fc_page) || SpGistPageIsDeleted(fc_page) || PageIsEmpty(fc_page))
		{
			/* 初始化页面是可以的 */
			uint16		fc_pageflags = 0;

			if (GBUF_REQ_LEAF(fc_flags))
				fc_pageflags |= SPGIST_LEAF;
			if (GBUF_REQ_NULLS(fc_flags))
				fc_pageflags |= SPGIST_NULLS;
			SpGistInitBuffer(fc_buffer, fc_pageflags);
			fc_lup->freeSpace = PageGetExactFreeSpace(fc_page) - fc_needSpace;
			*fc_isNew = true;
			return fc_buffer;
		}

		/*
		 * 检查页面类型是否正确且空间是否足够。我们必须
		 * 重新检查这一点，因为我们的缓存不一定是最新的。
		 */
		if ((GBUF_REQ_LEAF(fc_flags) ? SpGistPageIsLeaf(fc_page) : !SpGistPageIsLeaf(fc_page)) &&
			(GBUF_REQ_NULLS(fc_flags) ? SpGistPageStoresNulls(fc_page) : !SpGistPageStoresNulls(fc_page)))
		{
			int			fc_freeSpace = PageGetExactFreeSpace(fc_page);

			if (fc_freeSpace >= fc_needSpace)
			{
				/* 成功，更新空闲空间信息并返回缓冲区 */
				fc_lup->freeSpace = fc_freeSpace - fc_needSpace;
				*fc_isNew = false;
				return fc_buffer;
			}
		}

		/*
		 * 回退到分配新缓冲区
		 */
		UnlockReleaseBuffer(fc_buffer);
	}

	/* 在缓存中没有成功，因此返回一个新的缓冲区 */
	*fc_isNew = true;
	return fc_allocNewBuffer(fc_index, fc_flags);
}

/*
 * 修改页面完成后更新 lastUsedPages 缓存。
 *
 * 如果缓存中已经包含该页面（其 freeSpace 可能已过时），
 * 或者如果该页面的空间大于我们缓存的内容，我们更新相应的缓存条目。
 */
void SpGistSetLastUsedPage(Relation fc_index, Buffer fc_buffer)
{
	SpGistCache *fc_cache = spgGetCache(fc_index);
	SpGistLastUsedPage *fc_lup;
	int			fc_freeSpace;
	Page		fc_page = BufferGetPage(fc_buffer);
	BlockNumber fc_blkno = BufferGetBlockNumber(fc_buffer);
	int			fc_flags;

	/* 但是决不要在缓存中进入固定页面（根页面） */
	if (SpGistBlockIsFixed(fc_blkno))
		return;

	if (SpGistPageIsLeaf(fc_page))
		fc_flags = GBUF_LEAF;
	else
		fc_flags = GBUF_INNER_PARITY(fc_blkno);
	if (SpGistPageStoresNulls(fc_page))
		fc_flags |= GBUF_NULLS;

	fc_lup = GET_LUP(fc_cache, fc_flags);

	fc_freeSpace = PageGetExactFreeSpace(fc_page);
	if (fc_lup->blkno == InvalidBlockNumber || fc_lup->blkno == fc_blkno ||
		fc_lup->freeSpace < fc_freeSpace)
	{
		fc_lup->blkno = fc_blkno;
		fc_lup->freeSpace = fc_freeSpace;
	}
}

/*
 * 将 SPGiST 页面初始化为空，并带有指定的标志
 */
void SpGistInitPage(Page fc_page, uint16 fc_f)
{
	SpGistPageOpaque fc_opaque;

	PageInit(fc_page, BLCKSZ, sizeof(SpGistPageOpaqueData));
	fc_opaque = SpGistPageGetOpaque(fc_page);
	fc_opaque->flags = fc_f;
	fc_opaque->spgist_page_id = SPGIST_PAGE_ID;
}

/*
 * 将缓冲区的页面初始化为空，并带有指定的标志
 */
void SpGistInitBuffer(Buffer fc_b, uint16 fc_f)
{
	Assert(BufferGetPageSize(fc_b) == BLCKSZ);
	SpGistInitPage(BufferGetPage(fc_b), fc_f);
}

/*
 * 初始化元数据页面
 */
void SpGistInitMetapage(Page fc_page)
{
	SpGistMetaPageData *fc_metadata;
	int			fc_i;

	SpGistInitPage(fc_page, SPGIST_META);
	fc_metadata = SpGistPageGetMeta(fc_page);
	memset(fc_metadata, 0, sizeof(SpGistMetaPageData));
	fc_metadata->magicNumber = SPGIST_MAGIC_NUMBER;

	/* 初始化 last-used-page 缓存为空 */
	for (fc_i = 0; fc_i < SPGIST_CACHED_PAGES; fc_i++)
		fc_metadata->lastUsedPages.cachedPage[fc_i].blkno = InvalidBlockNumber;

	/*
	 * 将 pd_lower 设置在元数据结束的后面。这是至关重要的，因为不这样做，如果 xlog.c 压缩页面，元数据将丢失。
	 */
	((PageHeader) fc_page)->pd_lower =
		((char *) fc_metadata + sizeof(SpGistMetaPageData)) - (char *) fc_page;
}

/*
 * SPGiST 的 reloptions 处理
 */
bytea * spgoptions(Datum fc_reloptions, bool fc_validate)
{
	static const relopt_parse_elt fc_tab[] = {
		{"fillfactor", RELOPT_TYPE_INT, offsetof(SpGistOptions, fillfactor)},
	};

	return (bytea *) build_reloptions(fc_reloptions, fc_validate,
									  RELOPT_KIND_SPGIST,
									  sizeof(SpGistOptions),
									  fc_tab, lengthof(fc_tab));
}

/*
 * 获取存储指定类型的非空数据项在内部元组中所需的空间
 * （即作为前缀或节点标签）。
 * 请注意，结果已经向上取整到 MAXALIGN 边界。
 * 在这里我们遵循传值类型仅存储在其 Datum 表示形式中的约定
 * （比较 memcpyInnerDatum）。
 */
unsigned int SpGistGetInnerTypeSize(SpGistTypeDesc *fc_att, Datum fc_datum)
{
	unsigned int fc_size;

	if (fc_att->attbyval)
		fc_size = sizeof(Datum);
	else if (fc_att->attlen > 0)
		fc_size = fc_att->attlen;
	else
		fc_size = VARSIZE_ANY(fc_datum);

	return MAXALIGN(fc_size);
}

/*
 * 将给定的非空数据项复制到 *target，在内部元组的情况下
 */
static void fc_memcpyInnerDatum(void *fc_target, SpGistTypeDesc *fc_att, Datum fc_datum)
{
	unsigned int fc_size;

	if (fc_att->attbyval)
	{
		memcpy(fc_target, &fc_datum, sizeof(Datum));
	}
	else
	{
		fc_size = (fc_att->attlen > 0) ? fc_att->attlen : VARSIZE_ANY(fc_datum);
		memcpy(fc_target, DatumGetPointer(fc_datum), fc_size);
	}
}

/*
 * 计算持有给定数据的叶子元组所需的空间。
 *
 * 这必须与 spgFormLeafTuple 的大小计算部分匹配。
 */
Size SpGistGetLeafTupleSize(TupleDesc fc_tupleDescriptor,
					   Datum *fc_datums, bool *fc_isnulls)
{
	Size		fc_size;
	Size		fc_data_size;
	bool		fc_needs_null_mask = false;
	int			fc_natts = fc_tupleDescriptor->natts;

	/*
	 * 决定我们是否需要空值位掩码。
	 *
	 * 如果只有一个关键属性（natts == 1），则永远不要使用位掩码，
	 * 以兼容 v14 之前的叶子元组布局。否则，如果有任何属性为空，则
	 * 我们需要一个位掩码。
	 */
	if (fc_natts > 1)
	{
		for (int fc_i = 0; fc_i < fc_natts; fc_i++)
		{
			if (fc_isnulls[fc_i])
			{
				fc_needs_null_mask = true;
				break;
			}
		}
	}

	/*
	 * 计算数据部分的大小；与堆元组相同。
	 */
	fc_data_size = heap_compute_data_size(fc_tupleDescriptor, fc_datums, fc_isnulls);

	/*
	 * 计算总大小。
	 */
	fc_size = SGLTHDRSZ(fc_needs_null_mask);
	fc_size += fc_data_size;
	fc_size = MAXALIGN(fc_size);

	/*
	 * 确保我们稍后可以用死元组替换该元组。当存在任何非空属性时，这一测试
	 * 是不必要的，但为了安全起见，还是要进行检测。
	 */
	if (fc_size < SGDTSIZE)
		fc_size = SGDTSIZE;

	return fc_size;
}

/*
 * 构造一个包含给定堆 TID 和数据值的叶子元组
 */
SpGistLeafTuple
spgFormLeafTuple(SpGistState *fc_state, ItemPointer fc_heapPtr,
				 Datum *fc_datums, bool *fc_isnulls)
{
	SpGistLeafTuple fc_tup;
	TupleDesc	fc_tupleDescriptor = fc_state->leafTupDesc;
	Size		fc_size;
	Size		fc_hoff;
	Size		fc_data_size;
	bool		fc_needs_null_mask = false;
	int			fc_natts = fc_tupleDescriptor->natts;
	char	   *fc_tp;				/* 指向元组数据的指针 */
	uint16		fc_tupmask = 0;	/* 未使用的 heap_fill_tuple 输出 */

	/*
	 * 决定我们是否需要空值位掩码。
	 *
	 * 如果只有一个关键属性（natts == 1），则永远不要使用位掩码，
	 * 以兼容 v14 之前的叶子元组布局。否则，如果有任何属性为空，则
	 * 我们需要一个位掩码。
	 */
	if (fc_natts > 1)
	{
		for (int fc_i = 0; fc_i < fc_natts; fc_i++)
		{
			if (fc_isnulls[fc_i])
			{
				fc_needs_null_mask = true;
				break;
			}
		}
	}

	/*
	 * 计算数据部分的大小；与堆元组相同。
	 */
	fc_data_size = heap_compute_data_size(fc_tupleDescriptor, fc_datums, fc_isnulls);

	/*
	 * 计算总大小。
	 */
	fc_hoff = SGLTHDRSZ(fc_needs_null_mask);
	fc_size = fc_hoff + fc_data_size;
	fc_size = MAXALIGN(fc_size);

	/*
	 * 确保我们稍后可以用死元组替换该元组。当存在任何非空属性时，这一测试
	 * 是不必要的，但为了安全起见，还是要进行检测。
	 */
	if (fc_size < SGDTSIZE)
		fc_size = SGDTSIZE;

	/* 好的，形成元组 */
	fc_tup = (SpGistLeafTuple) palloc0(fc_size);

	fc_tup->size = fc_size;
	SGLT_SET_NEXTOFFSET(fc_tup, InvalidOffsetNumber);
	fc_tup->heapPtr = *fc_heapPtr;

	fc_tp = (char *) fc_tup + fc_hoff;

	if (fc_needs_null_mask)
	{
		bits8	   *fc_bp;			/* 指向元组中 null 位图的指针 */

		/* 在 SpGistLeafTuple 头部设置空掩码存在位 */
		SGLT_SET_HASNULLMASK(fc_tup, true);
		/* 填充数据区域和空掩码 */
		fc_bp = (bits8 *) ((char *) fc_tup + sizeof(SpGistLeafTupleData));
		heap_fill_tuple(fc_tupleDescriptor, fc_datums, fc_isnulls, fc_tp, fc_data_size,
						&fc_tupmask, fc_bp);
	}
	else if (fc_natts > 1 || !fc_isnulls[spgKeyColumn])
	{
		/* 只填充数据区域 */
		heap_fill_tuple(fc_tupleDescriptor, fc_datums, fc_isnulls, fc_tp, fc_data_size,
						&fc_tupmask, (bits8 *) NULL);
	}
	/* 否则我们没有数据，也没有位图来填充 */

	return fc_tup;
}

/*
 * 构造一个节点（进入内层元组）包含给定标签
 *
 * 注意，节点的下行链接在这里被设置为无效。调用者会稍后填充它。
 */
SpGistNodeTuple
spgFormNodeTuple(SpGistState *fc_state, Datum fc_label, bool fc_isnull)
{
	SpGistNodeTuple fc_tup;
	unsigned int fc_size;
	unsigned short fc_infomask = 0;

	/* 计算所需的空间（注意结果已经是最大对齐的） */
	fc_size = SGNTHDRSZ;
	if (!fc_isnull)
		fc_size += SpGistGetInnerTypeSize(&fc_state->attLabelType, fc_label);

	/*
	 * 在这里，我们确保大小能够放入 t_info 中为其预留的字段。
	 */
	if ((fc_size & INDEX_SIZE_MASK) != fc_size)
		ereport(ERROR,
				(errcode(ERRCODE_PROGRAM_LIMIT_EXCEEDED),
				 errmsg("index row requires %zu bytes, maximum size is %zu",
						(Size) fc_size, (Size) INDEX_SIZE_MASK)));

	fc_tup = (SpGistNodeTuple) palloc0(fc_size);

	if (fc_isnull)
		fc_infomask |= INDEX_NULL_MASK;
	/* 我们不必设置 INDEX_VAR_MASK 位 */
	fc_infomask |= fc_size;
	fc_tup->t_info = fc_infomask;

	/* TID 字段将稍后填充 */
	ItemPointerSetInvalid(&fc_tup->t_tid);

	if (!fc_isnull)
		fc_memcpyInnerDatum(SGNTDATAPTR(fc_tup), &fc_state->attLabelType, fc_label);

	return fc_tup;
}

/*
 * 构造一个包含给定前缀和节点数组的内层元组
 */
SpGistInnerTuple
spgFormInnerTuple(SpGistState *fc_state, bool fc_hasPrefix, Datum fc_prefix,
				  int fc_nNodes, SpGistNodeTuple *fc_nodes)
{
	SpGistInnerTuple fc_tup;
	unsigned int fc_size;
	unsigned int fc_prefixSize;
	int			fc_i;
	char	   *fc_ptr;

	/* 计算所需的大小 */
	if (fc_hasPrefix)
		fc_prefixSize = SpGistGetInnerTypeSize(&fc_state->attPrefixType, fc_prefix);
	else
		fc_prefixSize = 0;

	fc_size = SGITHDRSZ + fc_prefixSize;

	/* 注意：我们依赖节点元组的大小已经最大对齐 */
	for (fc_i = 0; fc_i < fc_nNodes; fc_i++)
		fc_size += IndexTupleSize(fc_nodes[fc_i]);

	/*
	 * 确保我们可以稍后用一个死元组替换元组。考虑到当前元组布局，这个测试是不必要的，但为了安全起见。
	 */
	if (fc_size < SGDTSIZE)
		fc_size = SGDTSIZE;

	/*
	 * 内层元组应该足够小以适应一个页面
	 */
	if (fc_size > SPGIST_PAGE_CAPACITY - sizeof(ItemIdData))
		ereport(ERROR,
				(errcode(ERRCODE_PROGRAM_LIMIT_EXCEEDED),
				 errmsg("SP-GiST inner tuple size %zu exceeds maximum %zu",
						(Size) fc_size,
						SPGIST_PAGE_CAPACITY - sizeof(ItemIdData)),
				 errhint("Values larger than a buffer page cannot be indexed.")));

	/*
	 * 检查头字段的溢出——如果上面的成功，这可能不会失败，但我们要小心。
	 */
	if (fc_size > SGITMAXSIZE ||
		fc_prefixSize > SGITMAXPREFIXSIZE ||
		fc_nNodes > SGITMAXNNODES)
		elog(ERROR, "SPGiST inner tuple header field is too small");

	/* 好的，形成元组 */
	fc_tup = (SpGistInnerTuple) palloc0(fc_size);

	fc_tup->nNodes = fc_nNodes;
	fc_tup->prefixSize = fc_prefixSize;
	fc_tup->size = fc_size;

	if (fc_hasPrefix)
		fc_memcpyInnerDatum(SGITDATAPTR(fc_tup), &fc_state->attPrefixType, fc_prefix);

	fc_ptr = (char *) SGITNODEPTR(fc_tup);

	for (fc_i = 0; fc_i < fc_nNodes; fc_i++)
	{
		SpGistNodeTuple fc_node = fc_nodes[fc_i];

		memcpy(fc_ptr, fc_node, IndexTupleSize(fc_node));
		fc_ptr += IndexTupleSize(fc_node);
	}

	return fc_tup;
}

/*
 * 构造一个“死”元组以替换正在被删除的元组。
 *
 * 状态可以是 SPGIST_REDIRECT、SPGIST_DEAD 或 SPGIST_PLACEHOLDER。
 * 对于 REDIRECT 元组，必须提供一个指针（blkno+offset），并且
 * xid 字段会自动填充。
 *
 * 这在关键部分被调用，因此我们不使用 palloc；元组
 * 在预分配的存储中构建。它应该在可以进行不同参数的另一个调用之前被复制。
 */
SpGistDeadTuple
spgFormDeadTuple(SpGistState *fc_state, int fc_tupstate,
				 BlockNumber fc_blkno, OffsetNumber fc_offnum)
{
	SpGistDeadTuple fc_tuple = (SpGistDeadTuple) fc_state->deadTupleStorage;

	fc_tuple->tupstate = fc_tupstate;
	fc_tuple->size = SGDTSIZE;
	SGLT_SET_NEXTOFFSET(fc_tuple, InvalidOffsetNumber);

	if (fc_tupstate == SPGIST_REDIRECT)
	{
		ItemPointerSet(&fc_tuple->pointer, fc_blkno, fc_offnum);
		fc_tuple->xid = fc_state->myXid;
	}
	else
	{
		ItemPointerSetInvalid(&fc_tuple->pointer);
		fc_tuple->xid = InvalidTransactionId;
	}

	return fc_tuple;
}

/*
 * 将 SPGiST 叶元组转换为 Datum/isnull 数组。
 *
 * 调用者必须为输出数组分配足够的存储。
 * （INDEX_MAX_KEYS 条目应该足够。）
 */
void spgDeformLeafTuple(SpGistLeafTuple fc_tup, TupleDesc fc_tupleDescriptor,
				   Datum *fc_datums, bool *fc_isnulls, bool fc_keyColumnIsNull)
{
	bool		fc_hasNullsMask = SGLT_GET_HASNULLMASK(fc_tup);
	char	   *fc_tp;				/* 指向元组数据的指针 */
	bits8	   *fc_bp;				/* 指向元组中 null 位图的指针 */

	if (fc_keyColumnIsNull && fc_tupleDescriptor->natts == 1)
	{
		/*
		 * 简单情况：只有键属性，而且我们在空树中。元组头中的 hasNullsMask 位不应被设置
		 * （因此我们不能使用 index_deform_tuple_internal），但
		 * 尽管如此，结果是 NULL。
		 *
		 * 注意：目前这是死代码，因为 noplace 在只有键属性时调用此函数。但我们应该覆盖这种情况。
		 */
		Assert(!fc_hasNullsMask);

		fc_datums[spgKeyColumn] = (Datum) 0;
		fc_isnulls[spgKeyColumn] = true;
		return;
	}

	fc_tp = (char *) fc_tup + SGLTHDRSZ(fc_hasNullsMask);
	fc_bp = (bits8 *) ((char *) fc_tup + sizeof(SpGistLeafTupleData));

	index_deform_tuple_internal(fc_tupleDescriptor,
								fc_datums, fc_isnulls,
								fc_tp, fc_bp, fc_hasNullsMask);

	/*
	 * 元组中的键列 isnull 值应与调用者的 keyColumnIsNull 标志一致。
	 */
	Assert(fc_keyColumnIsNull == fc_isnulls[spgKeyColumn]);
}

/*
 * 提取 innerTuple 中节点的标签数据
 *
 * 如果标签数据为 NULL，则返回 NULL
 */
Datum * spgExtractNodeLabels(SpGistState *fc_state, SpGistInnerTuple fc_innerTuple)
{
	Datum	   *fc_nodeLabels;
	int			fc_i;
	SpGistNodeTuple fc_node;

	/* 所有标签必须为 NULL，或者都不为 NULL。 */
	fc_node = SGITNODEPTR(fc_innerTuple);
	if (IndexTupleHasNulls(fc_node))
	{
		SGITITERATE(fc_innerTuple, fc_i, fc_node)
		{
			if (!IndexTupleHasNulls(fc_node))
				elog(ERROR, "some but not all node labels are null in SPGiST inner tuple");
		}
		/* 它们都是空的，因此只需返回 NULL */
		return NULL;
	}
	else
	{
		fc_nodeLabels = (Datum *) palloc(sizeof(Datum) * fc_innerTuple->nNodes);
		SGITITERATE(fc_innerTuple, fc_i, fc_node)
		{
			if (IndexTupleHasNulls(fc_node))
				elog(ERROR, "some but not all node labels are null in SPGiST inner tuple");
			fc_nodeLabels[fc_i] = SGNTDATUM(fc_node, fc_state);
		}
		return fc_nodeLabels;
	}
}

/*
 * 将新项目添加到页面，如果可能的话替换PLACEHOLDER项目。
 * 返回插入的位置，失败时返回InvalidOffsetNumber。
 *
 * 如果startOffset不是NULL，我们从*startOffset开始搜索占位符，
 * 并将其更新到下一个搜索位置。这只是重复插入的优化。
 *
 * 如果errorOK为false，当空间不足时我们抛出错误，
 * 而不是返回InvalidOffsetNumber。
 */
OffsetNumber SpGistPageAddNewItem(SpGistState *fc_state, Page fc_page, Item fc_item, Size fc_size,
					 OffsetNumber *fc_startOffset, bool fc_errorOK)
{
	SpGistPageOpaque fc_opaque = SpGistPageGetOpaque(fc_page);
	OffsetNumber fc_i,
				fc_maxoff,
				fc_offnum;

	if (fc_opaque->nPlaceholder > 0 &&
		PageGetExactFreeSpace(fc_page) + SGDTSIZE >= MAXALIGN(fc_size))
	{
		/* 尝试替换占位符 */
		fc_maxoff = PageGetMaxOffsetNumber(fc_page);
		fc_offnum = InvalidOffsetNumber;

		for (;;)
		{
			if (fc_startOffset && *fc_startOffset != InvalidOffsetNumber)
				fc_i = *fc_startOffset;
			else
				fc_i = FirstOffsetNumber;
			for (; fc_i <= fc_maxoff; fc_i++)
			{
				SpGistDeadTuple fc_it = (SpGistDeadTuple) PageGetItem(fc_page,
																   PageGetItemId(fc_page, fc_i));

				if (fc_it->tupstate == SPGIST_PLACEHOLDER)
				{
					fc_offnum = fc_i;
					break;
				}
			}

			/* 如果找到了占位符则完成 */
			if (fc_offnum != InvalidOffsetNumber)
				break;

			if (fc_startOffset && *fc_startOffset != InvalidOffsetNumber)
			{
				/* 提示无效，从头开始重新搜索 */
				*fc_startOffset = InvalidOffsetNumber;
				continue;
			}

			/* 嗯，没有找到占位符？ */
			fc_opaque->nPlaceholder = 0;
			break;
		}

		if (fc_offnum != InvalidOffsetNumber)
		{
			/* 替换占位符元组 */
			PageIndexTupleDelete(fc_page, fc_offnum);

			fc_offnum = PageAddItem(fc_page, fc_item, fc_size, fc_offnum, false, false);

			/*
			 * 考虑到函数顶部的大小检查，我们不应该失败，但还是测试一下。
			 * 如果我们确实失败了，我们必须PANIC，因为我们已经删除了占位符元组，
			 * 而且没有其他方法可以防止损坏写入磁盘。
			 */
			if (fc_offnum != InvalidOffsetNumber)
			{
				Assert(fc_opaque->nPlaceholder > 0);
				fc_opaque->nPlaceholder--;
				if (fc_startOffset)
					*fc_startOffset = fc_offnum + 1;
			}
			else
				elog(PANIC, "failed to add item of size %zu to SPGiST index page",
					 fc_size);

			return fc_offnum;
		}
	}

	/* 替换占位符失败，所以只需将其添加到页面 */
	fc_offnum = PageAddItem(fc_page, fc_item, fc_size,
						 InvalidOffsetNumber, false, false);

	if (fc_offnum == InvalidOffsetNumber && !fc_errorOK)
		elog(ERROR, "failed to add item of size %zu to SPGiST index page",
			 fc_size);

	return fc_offnum;
}

/*
 *	spgproperty() -- 检查索引的布尔属性。
 *
 * 对于大多数AM来说这是可选的，但对于SP-GiST来说是必需的，
 * 因为核心属性代码不支持AMPROP_DISTANCE_ORDERABLE。
 */
bool spgproperty(Oid fc_index_oid, int fc_attno,
			IndexAMProperty fc_prop, const char *fc_propname,
			bool *fc_res, bool *fc_isnull)
{
	Oid			fc_opclass,
				fc_opfamily,
				fc_opcintype;
	CatCList   *fc_catlist;
	int			fc_i;

	/* 仅回答列级查询 */
	if (fc_attno == 0)
		return false;

	switch (fc_prop)
	{
		case AMPROP_DISTANCE_ORDERABLE:
			break;
		default:
			return false;
	}

	/*
	 * 目前，SP-GiST距离有序扫描要求操作类中存在带有默认类型的距离运算符。
	 * 所以我们假设如果存在这样的运算符，那么它是有理由的。
	 */

	/* 首先，我们需要知道列的操作类。 */
	fc_opclass = get_index_column_opclass(fc_index_oid, fc_attno);
	if (!OidIsValid(fc_opclass))
	{
		*fc_isnull = true;
		return true;
	}

	/* 现在查找操作类的族和输入数据类型。 */
	if (!get_opclass_opfamily_and_input_type(fc_opclass, &fc_opfamily, &fc_opcintype))
	{
		*fc_isnull = true;
		return true;
	}

	/* 现在我们可以检查是否提供了运算符。 */
	fc_catlist = SearchSysCacheList1(AMOPSTRATEGY,
								  ObjectIdGetDatum(fc_opfamily));

	*fc_res = false;

	for (fc_i = 0; fc_i < fc_catlist->n_members; fc_i++)
	{
		HeapTuple	fc_amoptup = &fc_catlist->members[fc_i]->tuple;
		Form_pg_amop fc_amopform = (Form_pg_amop) GETSTRUCT(fc_amoptup);

		if (fc_amopform->amoppurpose == AMOP_ORDER &&
			(fc_amopform->amoplefttype == fc_opcintype ||
			 fc_amopform->amoprighttype == fc_opcintype) &&
			opfamily_can_sort_type(fc_amopform->amopsortfamily,
								   get_op_rettype(fc_amopform->amopopr)))
		{
			*fc_res = true;
			break;
		}
	}

	ReleaseSysCacheList(fc_catlist);

	*fc_isnull = false;

	return true;
}
